package com.ljc.db.backend.dm;

import com.google.common.primitives.Bytes;
import com.ljc.db.backend.common.SubArray;
import com.ljc.db.backend.dm.dataItem.DataItem;
import com.ljc.db.backend.dm.logger.Logger;
import com.ljc.db.backend.dm.page.Page;
import com.ljc.db.backend.dm.page.PageX;
import com.ljc.db.backend.dm.pageCache.PageCache;
import com.ljc.db.backend.tm.TransactionManager;
import com.ljc.db.backend.utils.Panic;
import com.ljc.db.backend.utils.Parser;

import java.util.*;
import java.util.Map.Entry;

public class Recover {

    private static final byte LOG_TYPE_INSERT = 0;
    private static final byte LOG_TYPE_UPDATE = 1;

    private static final int REDO = 0;
    private static final int UNDO = 1;

    /**
     * [LogType] [XID] [Pgno] [Offset] [Raw]
     */
    static class InsertLogInfo {
        long xid;       // 事务id
        int pgno;       // 插入数据对应的文件页
        short offset;   // 偏移量
        byte[] raw;     // 要插入的数据
    }

    /**
     * [LogType] [XID] [UID] [OldRaw] [NewRaw]
     */
    static class UpdateLogInfo {
        long xid;       // 事务id
        int pgno;       // 更新数据对应的文件页
        short offset;   // 偏移量
        byte[] oldRaw;  // 原来的数据
        byte[] newRaw;  // 要更新的数据
    }

    /**
     * 根据.log文件中的记录，恢复数据库中的数据
     */
    public static void recover(TransactionManager tm, Logger lg, PageCache pc) {
        System.out.println("Recovering...");
        // 先将指针指向第一条日志
        lg.rewind();
        int maxPgno = 0;
        // 循环读取日志
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            int pgno;
            if(isInsertLog(log)) {
                InsertLogInfo li = parseInsertLog(log);
                pgno = li.pgno;
            } else {
                UpdateLogInfo li = parseUpdateLog(log);
                pgno = li.pgno;
            }
            // 更新 maxPgno 为最大页数
            if(pgno > maxPgno) {
                maxPgno = pgno;
            }
        }
        // 数据页至少有 1 页
        if(maxPgno == 0) {
            maxPgno = 1;
        }
        // 根据.log文件中的记录，先将.db文件设置为能保存所有日志中操作所使用的页数量
        pc.truncateByBgno(maxPgno);
        System.out.println("Truncate to " + maxPgno + " pages.");
        // 重做 redo
        redoTransactions(tm, lg, pc);
        System.out.println("Redo Transactions Over.");
        // 撤销 undo
        undoTransactions(tm, lg, pc);
        System.out.println("Undo Transactions Over.");

        System.out.println("Recovery Over.");
    }

    /**
     * 重做已完成的事务【committed或aborted】
     */
    private static void redoTransactions(TransactionManager tm, Logger lg, PageCache pc) {
        // 指向日志文件的第一条日志
        lg.rewind();
        while(true) {
            // 循环获得每一条日志
            byte[] log = lg.next();
            if(log == null) break;
            if(isInsertLog(log)) {
                // 如果是记录插入操作的日志，对其进行解析，获得解析好的信息
                InsertLogInfo li = parseInsertLog(log);
                long xid = li.xid;
                // 调用TM模块的方法，查看事务.xid文件中记录的该事务id的信息
                // 【如果不为活跃状态，则redo插入操作】
                if(!tm.isActive(xid)) {
                    // redo插入操作
                    doInsertLog(pc, log, REDO);
                }
            } else {
                // 否则就是更新操作的日志，对其进行解析，获得解析好的信息
                UpdateLogInfo xi = parseUpdateLog(log);
                long xid = xi.xid;
                // 【如果不为活跃状态，则redo更新操作】
                if(!tm.isActive(xid)) {
                    doUpdateLog(pc, log, REDO);
                }
            }
        }
    }

    /**
     * 撤销活跃状态的事务【active]
     */
    private static void undoTransactions(TransactionManager tm, Logger lg, PageCache pc) {
        Map<Long, List<byte[]>> logCache = new HashMap<>();
        lg.rewind();
        // 从前向后正序遍历【寻找活跃状态的事物】
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            // 判断是否为插入类型的日志
            if(isInsertLog(log)) {
                InsertLogInfo li = parseInsertLog(log);
                long xid = li.xid;
                if(tm.isActive(xid)) {
                    // 找到活跃状态的事务
                    if(!logCache.containsKey(xid)) {
                        // 如果该事务以前没有出现过，首先创建一个链表用于保存此次事务中的各种操作
                        logCache.put(xid, new ArrayList<>());
                    }
                    // 将日志放入该 xid 对应的链表中
                    logCache.get(xid).add(log);
                }
            } else {
                // 操作更新类型的日志【和插入类型相同】
                UpdateLogInfo xi = parseUpdateLog(log);
                long xid = xi.xid;
                if(tm.isActive(xid)) {
                    if(!logCache.containsKey(xid)) {
                        logCache.put(xid, new ArrayList<>());
                    }
                    logCache.get(xid).add(log);
                }
            }
        }

        // 对所有active log进行倒序undo
        for(Entry<Long, List<byte[]>> entry : logCache.entrySet()) {
            // 获得所有活跃事务，将所有活跃事务进行 undo 操作
            // 【同一个事务下可以有多个操作，一个xid可以对应多个log】
            List<byte[]> logs = entry.getValue();
            for (int i = logs.size()-1; i >= 0; i --) {
                byte[] log = logs.get(i);
                if(isInsertLog(log)) {
                    doInsertLog(pc, log, UNDO);
                } else {
                    doUpdateLog(pc, log, UNDO);
                }
            }
            // 回滚【将所有活跃状态的事物设置为abort，getKet()就是获得的事物id】
            tm.abort(entry.getKey());
        }
    }

    /**
     * 判断日志内容的首位是否为插入类型的日志
     */
    private static boolean isInsertLog(byte[] log) {
        return log[0] == LOG_TYPE_INSERT;
    }

    // [LogType] [XID] [UID] [OldRaw] [NewRaw]
    private static final int OF_TYPE = 0;   // 日志类型的起始位置 0
    private static final int OF_XID = OF_TYPE + 1;  // 事务id的起始位置 1
    private static final int OF_UPDATE_UID = OF_XID + 8;    // 更新操作id起始位置 9
    private static final int OF_UPDATE_RAW = OF_UPDATE_UID + 8; // 更新操作新字段的内容起始位置 17

    public static byte[] updateLog(long xid, DataItem di) {
        byte[] logType = {LOG_TYPE_UPDATE};
        byte[] xidRaw = Parser.long2Byte(xid);
        byte[] uidRaw = Parser.long2Byte(di.getUid());
        byte[] oldRaw = di.getOldRaw();
        SubArray raw = di.getRaw();
        byte[] newRaw = Arrays.copyOfRange(raw.raw, raw.start, raw.end);
        return Bytes.concat(logType, xidRaw, uidRaw, oldRaw, newRaw);
    }

    /**
     *  解析更新操作的日志
     */
    private static UpdateLogInfo parseUpdateLog(byte[] log) {
        UpdateLogInfo li = new UpdateLogInfo();
        li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_UPDATE_UID));
        long uid = Parser.parseLong(Arrays.copyOfRange(log, OF_UPDATE_UID, OF_UPDATE_RAW));
        li.offset = (short)(uid & ((1L << 16) - 1));
        uid >>>= 32;
        li.pgno = (int)(uid & ((1L << 32) - 1));
        int length = (log.length - OF_UPDATE_RAW) / 2;
        li.oldRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW, OF_UPDATE_RAW+length);
        li.newRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW+length, OF_UPDATE_RAW+length*2);
        return li;
    }

    /**
     * 根据插入日志和事务是否活跃选择 redo 或 undo 更新操作
     */
    private static void doUpdateLog(PageCache pc, byte[] log, int flag) {
        int pgno;
        short offset;
        byte[] raw;
        // redo 操作
        if(flag == REDO) {
            UpdateLogInfo xi = parseUpdateLog(log);
            pgno = xi.pgno;
            offset = xi.offset;
            // 将原数据设置为更新的数据
            raw = xi.newRaw;
        } else {
            // undo 操作
            UpdateLogInfo xi = parseUpdateLog(log);
            pgno = xi.pgno;
            offset = xi.offset;
            // 将原数据设置为未更新的数据
            raw = xi.oldRaw;
        }
        Page pg = null;
        try {
            pg = pc.getPage(pgno);
        } catch (Exception e) {
            Panic.panic(e);
        }
        try {
            // 将数据写入.db文件中
            PageX.recoverUpdate(pg, raw, offset);
        } finally {
            pg.release();
        }
    }

    // [LogType] [XID] [Pgno] [Offset] [Raw]
    private static final int OF_INSERT_PGNO = OF_XID + 8;   // 插入操作的页号起始位置 9
    private static final int OF_INSERT_OFFSET = OF_INSERT_PGNO + 4; // 插入位置的偏移量位置 13
    private static final int OF_INSERT_RAW = OF_INSERT_OFFSET + 2;  // 插入的数据位置 15

    /**
     * 生成插入日志
     */
    public static byte[] insertLog(long xid, Page pg, byte[] raw) {
        byte[] logTypeRaw = {LOG_TYPE_INSERT};
        byte[] xidRaw = Parser.long2Byte(xid);
        // 获得当前要插入的页号
        byte[] pgnoRaw = Parser.int2Byte(pg.getPageNumber());
        // 根据空闲位置偏移量，将确定插入数据的插入位置
        byte[] offsetRaw = Parser.short2Byte(PageX.getFSO(pg));
        return Bytes.concat(logTypeRaw, xidRaw, pgnoRaw, offsetRaw, raw);
    }

    /**
     * 对insert类型的日志进行解析，获得其中的xid，pgno，offset，raw，最后返回解析好的信息
     */
    private static InsertLogInfo parseInsertLog(byte[] log) {
        InsertLogInfo li = new InsertLogInfo();
        li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_INSERT_PGNO));
        li.pgno = Parser.parseInt(Arrays.copyOfRange(log, OF_INSERT_PGNO, OF_INSERT_OFFSET));
        li.offset = Parser.parseShort(Arrays.copyOfRange(log, OF_INSERT_OFFSET, OF_INSERT_RAW));
        li.raw = Arrays.copyOfRange(log, OF_INSERT_RAW, log.length);
        return li;
    }

    /**
     * 根据插入日志和事务是否活跃选择 redo 或 undo 插入操作
     */
    private static void doInsertLog(PageCache pc, byte[] log, int flag) {
        InsertLogInfo li = parseInsertLog(log);
        Page pg = null;
        try {
            pg = pc.getPage(li.pgno);
        } catch(Exception e) {
            Panic.panic(e);
        }
        try {
            if(flag == UNDO) {
                // 如果为 undo 操作，就会把这次事务中的操作视为无效【进行逻辑删除】
                DataItem.setDataItemRawInvalid(li.raw);
            }
            // 否则就会 redo 重做日志中记录的本次事务中的操作
            PageX.recoverInsert(pg, li.raw, li.offset);
        } finally {
            // 最后将缓存释放掉【同时也把缓存页持久化到磁盘中】
            pg.release();
        }
    }
}
